package com.zeyu.framework.core.persistence.table;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>
 * POJO that wraps all the parameters sent by Datatables to the server when
 * server-side processing is enabled. This bean can then be used to build SQL
 * queries.
 * </p>
 */
public class DatatablesCriterias implements Serializable {

    /**
     * The Constant serialVersionUID.
     */
    private static final long serialVersionUID = 1L;

    /**
     * The Constant LOG.
     */
    private static final Logger LOG = LoggerFactory
            .getLogger(DatatablesCriterias.class);

    // AJAX URL parameters
    /**
     * The Constant DT_I_DRAW.
     */
    public static final String DT_I_DRAW = "draw";

    /**
     * The Constant DT_I_START.
     */
    public static final String DT_I_START = "start";

    /**
     * The Constant DT_I_LENGTH.
     */
    public static final String DT_I_LENGTH = "length";

    /**
     * The Constant DT_S_SEARCH.
     */
    public static final String DT_S_SEARCH = "search[value]";

    /**
     * The Constant DT_S_EXTRA_SEARCH.
     */
    public static final String DT_S_EXTRA_SEARCH = "extra_search";

    /**
     * The search.
     */
    private final String search;

    /**
     * The search.
     */
    private final String extraSearch;

    /**
     * The start.
     */
    private Integer start;

    /**
     * The length.
     */
    private Integer length;

    /**
     * The column defs.
     */
    private final List<ColumnDef> columnDefs;

    /**
     * The sorting column defs.
     */
    private final List<ColumnDef> sortingColumnDefs;

    /**
     * The draw.
     */
    private final Integer draw;

    /**
     * Instantiates a new datatables criterias.
     *
     * @param search            the search
     * @param displayStart      the display start
     * @param displaySize       the display size
     * @param columnDefs        the column defs
     * @param sortingColumnDefs the sorting column defs
     * @param draw              the draw
     */
    private DatatablesCriterias(String search, String extraSearch, Integer displayStart,
                                Integer displaySize, List<ColumnDef> columnDefs,
                                List<ColumnDef> sortingColumnDefs, Integer draw) {
        this.search = search;
        this.extraSearch = extraSearch;
        this.start = displayStart;
        this.length = displaySize;
        this.columnDefs = columnDefs;
        this.sortingColumnDefs = sortingColumnDefs;
        this.draw = draw;
    }

    /**
     * Gets the start.
     *
     * @return the start
     */
    public Integer getStart() {
        return start;
    }

    /**
     * Gets the length.
     *
     * @return the length
     */
    public Integer getLength() {
        return length;
    }

    /**
     * Gets the search.
     *
     * @return the search
     */
    public String getSearch() {
        return search;
    }

    /**
     * @return extra search
     */
    public String getExtraSearch() {
        return extraSearch;
    }

    /**
     * Gets the draw.
     *
     * @return the draw
     */
    public Integer getDraw() {
        return draw;
    }

    /**
     * set query start
     */
    public void setStart(Integer start) {
        this.start = start;
    }

    /**
     * set query seize
     */
    public void setLength(Integer length) {
        this.length = length;
    }

    /**
     * Gets the column defs.
     *
     * @return the column defs
     */
    public List<ColumnDef> getColumnDefs() {
        return columnDefs;
    }

    /**
     * Gets the sorting column defs.
     *
     * @return the sorting column defs
     */
    public List<ColumnDef> getSortingColumnDefs() {
        return sortingColumnDefs;
    }

    /**
     * Checks for one filterable column.
     *
     * @return true if a column is filterable, false otherwise.
     */
    public Boolean hasOneFilterableColumn() {
        for (ColumnDef columnDef : this.columnDefs) {
            if (columnDef.isFilterable()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks for one filtered column.
     *
     * @return true if a column is being filtered, false otherwise.
     */
    public Boolean hasOneFilteredColumn() {
        for (ColumnDef columnDef : this.columnDefs) {
            if (StringUtils.isNotBlank(columnDef.getSearch())
                    || StringUtils.isNotBlank(columnDef.getSearchFrom())
                    || StringUtils.isNotBlank(columnDef.getSearchTo())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Checks for one sorted column.
     *
     * @return true if a column is being sorted, false otherwise.
     */
    public Boolean hasOneSortedColumn() {
        return !sortingColumnDefs.isEmpty();
    }

    /**
     * Construct the LIMIT clause for server-side processing SQL query
     */
    public String getLimit() {
        StringBuilder limit = new StringBuilder("");

        if (start > -1 && length > 0) {
            limit.append(" LIMIT ");
            limit.append(start);
            limit.append(", ");
            limit.append(length);
        }

        return limit.toString();
    }

    /**
     * Construct the ORDER BY clause for server-side processing SQL query
     * <p/>
     * <pre>
     * if not exist old order:
     * 		such as: ORDER BY Company DESC
     * 		such as: ORDER BY Company DESC, OrderNumber ASC
     * if exist old order:
     * 		such as: , Company DESC
     * 		such as: , Company DESC, OrderNumber ASC
     * </pre>
     */
    public String getOrder(boolean existOldOrder) {

        StringBuilder order = new StringBuilder("");

        if (sortingColumnDefs != null && sortingColumnDefs.size() > 0) {
            if (existOldOrder) {
                order.append(", ");
            } else {
                order.append(" ORDER BY ");
            }

            for (int i = 0; i < sortingColumnDefs.size(); i++) {
                order.append(sortingColumnDefs.get(i).getName());
                order.append(" ");
                order.append(sortingColumnDefs.get(i).getSortDirection());
                if (i == sortingColumnDefs.size() - 1) {
                    order.append(" ");
                } else {
                    order.append(", ");
                }
            }
        }

        return order.toString();
    }

    /**
     * Construct the WHERE clause for server-side processing SQL query.
     * <p/>
     * NOTE this does not match the built-in DataTables filtering which does it
     * word by word on any field. It's possible to do here performance on large
     * databases would be very poor
     * <p/>
     * <pre>
     * if not exist old where:
     * 		such as: WHERE ( `Company` LIKE '%value%' OR `OrderNumber` LIKE '%value%' )
     * 		such as: WHERE ( `Company` LIKE '%value%' )
     * if exist old where:
     * 		such as: AND ( `Company` LIKE '%value%' OR `OrderNumber` LIKE '%value%' )
     * 		such as: AND ( `Company` LIKE '%value%' )
     * </pre>
     */
    public String getWhere(boolean existOldWhere) {

        List<String> filters = new ArrayList<>();
        // global search
        if (StringUtils.isNotEmpty(search)) {
            columnDefs.stream().filter(ColumnDef::isFilterable).forEach(columnDef -> {
                StringBuilder filter = new StringBuilder("");
                filter.append("`");
                filter.append(columnDef.getName());
                filter.append("`");
                filter.append(" LIKE ");
                filter.append("'%");
                filter.append(search);
                filter.append("%'");

                filters.add(filter.toString());
            });
        }

        if (filters.size() > 0) {
            StringBuilder where = new StringBuilder("");
            if (existOldWhere) {
                where.append(" AND ");
            } else {
                where.append(" WHERE ");
            }
            where.append("(");
            for (int i = 0; i < filters.size(); i++) {
                where.append(" ");
                where.append(filters.get(i));
                if (i == filters.size() - 1) {
                    where.append(" ");
                } else {
                    where.append(" OR ");
                }
            }
            where.append(")");

            return where.toString();
        }

        // column search not do.
        // ignore

        return " ";
    }

    /**
     * <p>
     * Map all request parameters into a wrapper POJO that eases SQL querying.
     * </p>
     *
     * @param request The request sent by Datatables containing all parameters.
     * @return a wrapper POJO.
     */
    public static DatatablesCriterias getFromRequest(HttpServletRequest request) {

        if (request == null) {
            throw new IllegalArgumentException(
                    "The HTTP request cannot be null");
        }

        int columnNumber = getColumnNumber(request);
        LOG.trace("Number of columns: " + columnNumber);

        String paramSearch = request.getParameter(DT_S_SEARCH);
        String paramDraw = request.getParameter(DT_I_DRAW);
        String paramStart = request.getParameter(DT_I_START);
        String paramLength = request.getParameter(DT_I_LENGTH);
        String paramExtraSearch = "";
        try {
            String exsearch = request.getParameter(DT_S_EXTRA_SEARCH);
            if (StringUtils.isNoneBlank(exsearch)) {
                paramExtraSearch = URLDecoder.decode(exsearch, "UTF-8");
            }
        } catch (UnsupportedEncodingException e) {
            LOG.error("", e);
        }

        Integer draw = StringUtils.isNotBlank(paramDraw) ? Integer
                .parseInt(paramDraw) : -1;
        Integer start = StringUtils.isNotBlank(paramStart) ? Integer
                .parseInt(paramStart) : -1;
        Integer length = StringUtils.isNotBlank(paramLength) ? Integer
                .parseInt(paramLength) : -1;

        // Column definitions
        List<ColumnDef> columnDefs = new ArrayList<>();

        for (int i = 0; i < columnNumber; i++) {

            ColumnDef columnDef = new ColumnDef();

            columnDef.setName(request.getParameter("columns[" + i + "][data]"));
            columnDef.setFilterable(Boolean.parseBoolean(request
                    .getParameter("columns[" + i + "][searchable]")));
            columnDef.setSortable(Boolean.parseBoolean(request
                    .getParameter("columns[" + i + "][orderable]")));
            columnDef.setRegex(request.getParameter("columns[" + i
                    + "][search][regex]"));

            String searchTerm = request.getParameter("columns[" + i
                    + "][search][value]");

            if (StringUtils.isNotBlank(searchTerm)) {
                columnDef.setFiltered(true);
                String[] splittedSearch = searchTerm.split("~");
                if ("~".equals(searchTerm)) {
                    columnDef.setSearch("");
                } else if (searchTerm.startsWith("~")) {
                    columnDef.setSearchTo(splittedSearch[1]);
                } else if (searchTerm.endsWith("~")) {
                    columnDef.setSearchFrom(splittedSearch[0]);
                } else if (searchTerm.contains("~")) {
                    columnDef.setSearchFrom(splittedSearch[0]);
                    columnDef.setSearchTo(splittedSearch[1]);
                } else {
                    columnDef.setSearch(searchTerm);
                }
            }

            columnDefs.add(columnDef);
        }

        // Sorted column definitions
        List<ColumnDef> sortingColumnDefs = new LinkedList<>();

        for (int i = 0; i < columnNumber; i++) {
            String paramSortedCol = request.getParameter("order[" + i
                    + "][column]");

            // The column is being sorted
            if (StringUtils.isNotBlank(paramSortedCol)) {
                Integer sortedCol = Integer.parseInt(paramSortedCol);
                ColumnDef sortedColumnDef = columnDefs.get(sortedCol);
                String sortedColDirection = request.getParameter("order[" + i
                        + "][dir]");
                if (StringUtils.isNotBlank(sortedColDirection)) {
                    sortedColumnDef.setSortDirection(ColumnDef.SortDirection
                            .valueOf(sortedColDirection.toUpperCase()));
                }

                sortingColumnDefs.add(sortedColumnDef);
            }
        }

        return new DatatablesCriterias(paramSearch, paramExtraSearch, start, length, columnDefs,
                sortingColumnDefs, draw);
    }

    /**
     * <p>
     * adefault date for wrapper POJO that eases SQL querying.
     * </p>
     *
     * @return a wrapper POJO.
     */
    public static DatatablesCriterias getNonCriterias() {

        // int columnNumber = 1;

        // default query
        String paramSearch = "";
        String paramDraw = "0";
        String paramStart = "0";
        String paramLength = "10";
        String paramExtraSearch = "";

        // default value
        Integer draw = StringUtils.isNotBlank(paramDraw) ? Integer
                .parseInt(paramDraw) : -1;
        Integer start = StringUtils.isNotBlank(paramStart) ? Integer
                .parseInt(paramStart) : -1;
        Integer length = StringUtils.isNotBlank(paramLength) ? Integer
                .parseInt(paramLength) : -1;

        // Column definitions
        List<ColumnDef> columnDefs = new ArrayList<>();

        // Sorted column definitions
        List<ColumnDef> sortingColumnDefs = new LinkedList<>();

        return new DatatablesCriterias(paramSearch, paramExtraSearch, start, length, columnDefs,
                sortingColumnDefs, draw);
    }

    /**
     * Gets the column number.
     *
     * @param request the request
     * @return the column number
     */
    private static int getColumnNumber(HttpServletRequest request) {

        //The pattern.
        Pattern pattern = Pattern.compile("columns\\[([0-9]*)?\\]");

        int columnNumber = 0;
        for (Enumeration<?> e = request.getParameterNames(); e
                .hasMoreElements(); ) {
            String param = (String) e.nextElement();
            Matcher matcher = pattern.matcher(param);
            while (matcher.find()) {
                Integer col = Integer.parseInt(matcher.group(1));
                if (col > columnNumber) {
                    columnNumber = col;
                }
            }
        }

        if (columnNumber != 0) {
            columnNumber++;
        }
        return columnNumber;
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }
}